Mot-clef const ✱ **************** Le mot clef *const* est un incontournable du langage C++. Dans du code professionnel, on peut même le classer dans le top 10 des mots-clefs utilisés. Pourtant, ce mot-clef disparaît généralement des études supérieures. Pourquoi un tel tabou ? * Ce mot clef a des interprétations multiples, il devient donc déroutant pour le développeur débutant. * Il est polluant au sens où l'insertion d'un mot-clef *const* dans le code va forcer le développeur à insérer ce mot clef à d'autres endroits du programme pour éviter des messages d'erreurs du compilateur. * Savoir comment réagir face à un problème relatif aux consts n'a rien d'évident. Cependant, ce mot-clef étant omniprésent dans les sources C++, nous allons quand même introduire et rappeler ces usages les plus fréquents. Const variable ============== .. panels:: :column: col-lg-5 p-2 **const** int NB_USERS_MAX = 5; Cette syntaxe permet de définir une constante : une valeur qui ne change pas. Déclarer une variable *const* permet au compilateur de la substituer par sa valeur afin de rendre l'exécution plus rapide. Cette technique permet aussi de rendre le code plus lisible, en effet le nom *NB_USERS_MAX* est bien plus parlant que le chiffre 5. Fonction membre const ===================== .. panels:: :column: col-lg-5 p-2 | class Z | { | ... fnt(...) **const** { ... } | }; En C++, une fonction *const* correspond à une fonction membre qui ne modifie pas l'état de l'objet sur lequel elle est appelée. Autrement dit, son appel n'entraîne pas de modification des variables membres de l'objet. On peut considérer que l'état de l'objet est resté constant durant l'appel de cette fonction. Ainsi cette syntaxe : * Sert de documentation aux programmeurs en indiquant que l'appel à cette fonction ne modifie pas l'objet. * Fournit une information au compilateur pour lever des messages d'erreur en cas de non respect des règles. Voici un exemple : .. code-block:: class MaClasse { private: int valeur; public: int obtenirValeur() const { return valeur; // lecture seule => on peut déclarer la fonction const } void changerValeur(int nouvelleValeur) { valeur = nouvelleValeur; // écrire => cette fonction ne peut être const } }; Const référence =============== .. panels:: :column: col-lg-5 p-2 void fnt(**const** int & nom) { ... } Voici un exemple : .. code-block:: void afficherValeur(const int& x) { cout << x << endl; // x = 5; => provoquerait une erreur de compilation } int main() { int a = 10; afficherValeur(a); } Une *const référence* en C++ est une référence qui ne permet d'appeler que des méthodes *const*. Ainsi, une fonction exposant une *const* référence en paramètre garantit qu'elle ne modifiera pas l'objet passé. L'utilisation de *const références* est courante, cela constitue : * Une documentation pour les programmeurs * Une sécurité car elle impose au compilateur de vérifier que le code respecte les contraintes associés au mot-clef *const*. Conversion ========== Dans cette logique : .. panels:: :column: col-lg-5 p-2 Une conversion implicite : * Référence => Const référence : autorisée * Const référence => référence : impossible Voici un exemple testant toutes les configurations possibles : .. code-block:: void ConstRef(const int & x) { std::cout << x << std::endl; } void NonConstRef(int & y) { y += 10; // Modification } int main() { int a = 5; int & ref = a; const int & cref = a; NonConstRef(ref); // OK ConstRef(ref); // OK NonConstRef(cref); // ERREUR ConstRef(cref); // OK } Référence vers une L-value ========================== Rappel ------ Ce cas rentre dans la catégorie : programmation avancée du C++. On aurait pu éviter d'en parler, mais comme vous allez le rencontrer, nous abordons maintenant le sujet à travers divers exemples utilisant la classe *Matrix*. .. code-block:: Matrix M = M1 + M2; Cet exemple fonctionne. Le déroulé est le suivant : * Appel de l'opérateur + prenant en arguments deux objets matrices. * Création et retour d'un objet matrice temporaire anonyme correspondant au résultat de l'addition. * Création et initialisation de la matrice *M* à partir de l'objet temporaire. * Destruction de l'objet temporaire .. code-block:: Matrix & M = M1 + M2; M.print(); Cette situation déclenche une erreur de compilation : en effet, on crée une référence sur un objet temporaire (une R-value) et le C++ l'interdit. Soit une erreur est levée à la compilation, soit un plantage peut avoir lieu à l'exécution, certainement le pire des cas car aucun message d'information n'est affiché. A la deuxième ligne de cet exemple, la référence *M* est associée à un objet probablement détruit. L'appel de la fonction *print()* déclenche alors une erreur générale. Cependant, il existe une autre option : .. panels:: :column: col-lg-10 p-2 Le langage C++ garantit qu'une *const* référence prolonge la durée de vie des objets temporaires. De plus, les *const* références peuvent se lier (bind) à des R-value (objet temporaire, littéral). Ainsi le code suivant fonctionnera : .. code-block:: const Matrix & M = M1 + M2; M.print(); // print() doit être une fonction const ! En pratique ----------- Ce problème des références non autorisées vers les L-values apparaît lors des utilisations de *cout*. Supposons que nous disposions des opérateurs suivants : .. code-block:: Matrix operator + (const Matrix & M1, const Matrix & M2) {... } ostream& operator<<(ostream& os, Matrix & mat) {...} // référence non const sur l opérande de droite En utilisant ces fonctions, vous allez pouvoir réaliser ceci : Scénario 1 ^^^^^^^^^^ .. code-block:: Matrix M1,M2; cout << M1 ; // passage d une L-value vers une référence non const : autorisé Scénario 2 ^^^^^^^^^^ .. code-block:: Matrix M1,M2; Matrix M = M1+M2; // initialisation d une L-value à partir d une R-value : autorisé cout << M; // passage d une L-value vers une référence non const : autorisé Scénario 3 ^^^^^^^^^^ .. code-block:: Matrix M1,M2; cout << M1+M2; // erreur générale Étrangement, alors que les deux premières options ne posent aucun problème, cette syntaxe conduit à l'accident (message d'erreur ou plantage). En effet, l'opérateur + retourne un objet temporaire (une R-Value) alors que l'opérande de droite de l'opérateur << accepte une référence non const, d'où le problème. Ainsi, pour que ces trois scénarios fonctionnement, **l'opérande de droite de l'opérateur << doit être une const référence** : .. code-block:: ostream& operator << (ostream& os, const Matrix & mat) {...} Le caractère polluant ===================== Cas 1 : propagation en arrière ------------------------------ .. code-block:: class T { ... public : void Aff() {...} int GetMin() {...} int GetMax() {...} }; void Test(const T & obj) { obj. ??? => AUCUNE OPTION POSSIBLE } int main () { T obj; Test(obj); } Si une fonction accepte une *const* référence, le compilateur vous autorise uniquement à appeler les méthodes *const* depuis cette référence, c'est logique. Si la classe n'expose aucune méthode *const*, la référence ne pourra appeler aucune méthode et là , c'est le drame ! Il faut donc reprendre la classe en question et déclarer *const* toutes les fonctions membres à caractère read-only. D'où la propagation en arrière du caractère *const* : .. code-block:: class T { ... public : void Aff() const {...} int GetMin() const {...} int GetMax() const {...} }; Cas 2 : dédoublement des opérateurs/fonctions --------------------------------------------- .. code-block:: class BoundCheckArray { int T[10]; public : BoundCheckArray() { for (int i = 0; i < 10; i++) T[i] = i; } int & operator[](int i) { if ((i<10) && (i>=0)) return T[i]; else throw std::out_of_range(""); } }; void Increase(BoundCheckArray & T) { for (int i = 0; i < 10; i++) T[i]++; } int main() { BoundCheckArray T; Increase(T); return 0; } L'opérateur d'indexation reçoit un entier comme index et retourne une référence (lvalue) afin de pouvoir éventuellement modifier la valeur contenue dans ce tableau. Si on avait simplement retourner un int (une r-value), on ne pourrait exécuter l'instruction : *T[2] = 4;* correctement par exemple. Le code de l'exemple fonctionne correctement. Supposons que l'on veuille maintenant utiliser une fonction affichant les éléments du tableau. Comme cette fonction ne modifie pas l'élément passé, elle prend donc une *const* référence en paramètre : .. code-block:: void Aff(const BoundCheckArray & T) { for (int i = 0; i < 10; i++) std::cout << T[i] << " "; } Or, comme *T* représente une *const* référence et que l'on accède à travers elle à l'opérateur [] *non const*, le compilateur lève une erreur. Il faut donc ajouter une deuxième version *const* de l'opérateur d'indexation. Que va retourner cette version *const* ? On peut lui faire retourner un *int* ce qui serait exact. Mais, si l'on avait un objet, cela produirait une copie. Il est alors un peu plus judicieux de retourner une *const* référence afin d'éviter cette recopie. Ainsi nous obtenons comme programme final : .. code-block:: #include <iostream> class BoundCheckArray { int T[10]; public : BoundCheckArray() { for (int i = 0; i < 10; i++) T[i] = i; } int & operator[](int i) { if ((i<10) && (i>=0)) return T[i]; else throw std::out_of_range(""); } const int & operator[](int i) const { if ((i<10) && (i>=0)) return T[i]; else throw std::out_of_range(""); } }; void Increase(BoundCheckArray & T) { for (int i = 0; i < 10; i++) T[i]++; // Appel de l opérateur [] non const } void Aff(const BoundCheckArray & T) { for (int i = 0; i < 10; i++) std::cout << T[i] << " "; // Appel de l opérateur [] const } int main() { BoundCheckArray T; Increase(T); Aff(T); return 0; }